//
//  VideoCaptureManager.swift
//  VideoPhotoCapture
//
//  Created by 杨伟诺 on 2020/9/26.
//

import AVFoundation
import AVKit

class VideoCaptureManager: NSObject {
    
    enum SessionSetUpSatus {
        case success
        case notAuthorized
        case configurationFailed
    }
    
    static var manager: VideoCaptureManager = VideoCaptureManager()
    
    var sessionQueue = DispatchQueue.init(label: "SessionQueue")
    
    var session = AVCaptureSession.init()
    
    var videoDeviceInput: AVCaptureDeviceInput!
    
    var audioDeviceInput: AVCaptureDeviceInput?
    
    private var photoOutput = AVCapturePhotoOutput()
    
    var sessionSetup: SessionSetUpSatus = .success
    
    var movieFileOutput: AVCaptureMovieFileOutput?
    
    var backgroundRecordingID: UIBackgroundTaskIdentifier?
    
    private let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes:
                                                                                [.builtInWideAngleCamera,
                                                                                 .builtInDualCamera,
                                                                                 .builtInTrueDepthCamera],
                                                                               mediaType: .video, position: .unspecified)

    func authorization(){
        switch AVCaptureDevice.authorizationStatus(for: AVMediaType.video) {
        case .authorized:
            break
            
        case .notDetermined:
            sessionQueue.suspend()
            AVCaptureDevice.requestAccess(for: .video) { (granded) in
                if !granded{
                    self.sessionSetup = .notAuthorized
                }
                self.sessionQueue.resume()
            }
            break
        default:
            sessionSetup = .notAuthorized
        }
    }
    
    func configuragionSession(setVideoOrientation: @escaping ()-> Void){
        
        guard sessionSetup == .success else {return}
        
        sessionQueue.async { [self] in
            session.beginConfiguration()
            session.sessionPreset = .photo
            do {
                var videoDevice: AVCaptureDevice?
                if let dualCameraDevice = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
                    videoDevice = dualCameraDevice
                } else if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
                    // If a rear dual camera is not available, default to the rear wide angle camera.
                    videoDevice = backCameraDevice
                } else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
                    // If the rear wide angle camera isn't available, default to the front wide angle camera.
                    videoDevice = frontCameraDevice
                }
                
                guard let vd = videoDevice else {
                    sessionSetup = .configurationFailed
                    session.commitConfiguration()
                    return
                }
                let vdInput = try AVCaptureDeviceInput.init(device: vd)
                
                if session.canAddInput(vdInput){
                    session.addInput(vdInput)
                    self.videoDeviceInput = vdInput
                    
                    DispatchQueue.main.async {
                        
                        setVideoOrientation()
                    }
                }
                else
                {
                    sessionSetup = .configurationFailed
                    session.commitConfiguration()
                    return
                }
            } catch  {
                sessionSetup = .configurationFailed
                session.commitConfiguration()
                return
            }
            
            do {
                if let aud = AVCaptureDevice.default(for: .audio){
                    let audInput = try AVCaptureDeviceInput.init(device: aud)
                    if session.canAddInput(audInput){
                        session.addInput(audInput)
                    }
                }
            } catch  {}
            
            // Add the photo output.
            if session.canAddOutput(photoOutput) {
                session.addOutput(photoOutput)
                
                photoOutput.isHighResolutionCaptureEnabled = true
                photoOutput.isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureSupported
                photoOutput.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliverySupported
                photoOutput.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliverySupported
                photoOutput.enabledSemanticSegmentationMatteTypes = photoOutput.availableSemanticSegmentationMatteTypes
                photoOutput.maxPhotoQualityPrioritization = .quality
                
            } else {
                sessionSetup = .configurationFailed
                session.commitConfiguration()
                return
            }
            
            session.commitConfiguration()
            session.startRunning()
        }
    }
    
    func changeToRecording(){
        sessionQueue.async {[self] in
            let mfo = AVCaptureMovieFileOutput()
            if session.canAddOutput(mfo){
                session.beginConfiguration()
                session.addOutput(mfo)
                session.sessionPreset = .high
                if let connection = mfo.connection(with: .video){
                    if connection.isVideoStabilizationSupported {
                        connection.preferredVideoStabilizationMode = .auto
                    }
                }
            }
            session.commitConfiguration()
            movieFileOutput = mfo
        }
    }
    
    func changeToCapturePhoto(){
        sessionQueue.async { [self] in
            session.beginConfiguration()
            if movieFileOutput != nil{
                //livephoto不支持moviefileOutput
                session.removeOutput(movieFileOutput!)
                movieFileOutput = nil
            }
            session.sessionPreset = .photo
            session.commitConfiguration()
        }
    }
    
    func changeCamera(){
        sessionQueue.async { [self] in
            let currentDevice = videoDeviceInput.device
            let currentPosition = currentDevice.position
            let perferredPosition: AVCaptureDevice.Position
            let perferredDeviceType: AVCaptureDevice.DeviceType
            
            switch currentPosition {
            case .unspecified, .front:
                perferredPosition = .back
                perferredDeviceType = .builtInDualCamera
                
            case .back:
                perferredPosition = .front
                perferredDeviceType = .builtInTrueDepthCamera
                
            default:
                perferredPosition = .back
                perferredDeviceType = .builtInDualCamera
            }
            
            let devices = videoDeviceDiscoverySession.devices
            var newVideoDevice: AVCaptureDevice? = nil
            
            if let dv = devices.first(where: { $0.position == perferredPosition && $0.deviceType == perferredDeviceType}){
                newVideoDevice = dv
            } else if let dv = devices.first(where: { $0.position == perferredPosition }){
                newVideoDevice = dv
            }
            
            if let videoDevice = newVideoDevice {
                
                do {
                    let vdi = try AVCaptureDeviceInput.init(device: videoDevice)
                    session.beginConfiguration()
                    
                    session.removeInput(videoDeviceInput)
                    
                    if session.canAddInput(vdi){
                        session.addInput(vdi)
                        self.videoDeviceInput = vdi
                    }
                    else {
                        session.addInput(videoDeviceInput)
                    }
                    
                    if let connection = movieFileOutput?.connection(with: .video){
                        if connection.isVideoStabilizationSupported{
                            connection.preferredVideoStabilizationMode = .auto
                        }
                    }
                    photoOutput.isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureSupported
                    photoOutput.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliverySupported
                    photoOutput.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliverySupported
                    photoOutput.enabledSemanticSegmentationMatteTypes = photoOutput.availableSemanticSegmentationMatteTypes
                    photoOutput.maxPhotoQualityPrioritization = .quality
                    
                    session.commitConfiguration()
                } catch  {}
            }
        }
    }
    
    func capturePhoto(videoOrientation: AVCaptureVideoOrientation, delegate: AVCapturePhotoCaptureDelegate){
        
        sessionQueue.async { [self] in
            if let poc = photoOutput.connection(with: .video){
                poc.videoOrientation = videoOrientation
            }
            
            var photoSettings = AVCapturePhotoSettings()
            if photoOutput.availablePhotoCodecTypes.contains(.hevc){
                photoSettings = AVCapturePhotoSettings.init(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
            }
            
            if self.videoDeviceInput.device.isFlashAvailable {
                photoSettings.flashMode = .auto
            }
            
            photoSettings.isHighResolutionPhotoEnabled = true
            
            if !photoSettings.availablePreviewPhotoPixelFormatTypes.isEmpty{
                photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.availablePreviewPhotoPixelFormatTypes.first!]
            }
            
            photoOutput.capturePhoto(with: photoSettings, delegate: delegate)
        }
    }
    
    func startRecording(videoOrientation: AVCaptureVideoOrientation, delegate: AVCaptureFileOutputRecordingDelegate){
        
        guard let movieFileOutput = self.movieFileOutput else {return}
        
        sessionQueue.async {
            
            if !movieFileOutput.isRecording{
                if UIDevice.current.isMultitaskingSupported {
                    self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
                }
                
                let movieFileOutputConnection = movieFileOutput.connection(with: .video)
                movieFileOutputConnection?.videoOrientation = videoOrientation
                
                let availableVideoCodyTypes = movieFileOutput.availableVideoCodecTypes
                if availableVideoCodyTypes.contains(.hevc){
                    movieFileOutput.setOutputSettings([AVVideoCodecKey:AVVideoCodecType.hevc], for: movieFileOutputConnection!)
                }
                
                let outputFileName = NSUUID().uuidString
                let outputFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((outputFileName as NSString).appendingPathExtension("mov")!)
                movieFileOutput.startRecording(to: URL.init(fileURLWithPath: outputFilePath), recordingDelegate: delegate)
            }
            else
            {
                movieFileOutput.stopRecording()
            }
        }
    }
}

